TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { notFound } from "next/navigation";3import { ArrowLeft, LifeBuoy } from "lucide-react";4import { ERROR_MESSAGES, type ErrorCode } from "@fetcha/core";5import { getWorkspace } from "@/lib/session";6import { formatBytes, formatDate, formatMs, formatUsd, timeAgo } from "@/lib/format";7import { getRequestDetail } from "@/lib/queries/dashboard";8import { PageHeader } from "@/components/ui/page-header";9import { Badge, StatusBadge } from "@/components/ui/badge";10import { Button } from "@/components/ui/button";11import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";12import { CopyButton } from "@/components/ui/copy-button";13import { Alert } from "@/components/ui/alert";14import { RequestDetailTabs } from "@/components/dashboard/requests/request-detail-tabs";15import { TimingWaterfall } from "@/components/dashboard/requests/timing-waterfall";16import { AttemptsTimeline } from "@/components/dashboard/requests/attempts-timeline";17import { HeadersTables } from "@/components/dashboard/requests/headers-table";18import { HttpCode, NetworkLabel } from "@/components/dashboard/requests/requests-table";1920export const dynamic = "force-dynamic";2122function Row({ label, children, mono }: { label: string; children: React.ReactNode; mono?: boolean }) {23 return (24 <div className="grid grid-cols-[140px_1fr] gap-3 px-4 py-2.5 text-[13px] sm:grid-cols-[180px_1fr]">25 <dt className="text-fg-subtle">{label}</dt>26 <dd className={`min-w-0 break-all ${mono ? "font-mono tabular" : ""}`}>{children}</dd>27 </div>28 );29}3031export default async function RequestDetailPage({ params }: { params: Promise<{ id: string }> }) {32 const [ws, { id }] = await Promise.all([getWorkspace(), params]);33 if (!/^req_[A-Za-z0-9]{4,64}$/.test(id)) notFound();34 const req = await getRequestDetail(ws.organization.id, id, { providerVisibility: ws.organization.providerVisibility });35 if (!req) notFound();3637 const headerCount = Object.keys(req.requestHeaders ?? {}).length + Object.keys(req.responseHeaders ?? {}).length;38 const errorHelp = req.errorCode && req.errorCode in ERROR_MESSAGES ? ERROR_MESSAGES[req.errorCode as ErrorCode] : null;39 const otherProject = req.projectId !== ws.project.id;4041 const overview = (42 <div className="grid gap-4 lg:grid-cols-2">43 <Card>44 <CardHeader className="pb-1">45 <CardTitle className="text-[14px]">Request</CardTitle>46 </CardHeader>47 <dl className="divide-y divide-border">48 <Row label="URL" mono>49 <a href={req.url} target="_blank" rel="noreferrer noopener" className="text-accent underline-offset-4 hover:underline">50 {req.url}51 </a>52 </Row>53 {req.finalUrl && req.finalUrl !== req.url ? (54 <Row label="Final URL" mono>55 {req.finalUrl}56 </Row>57 ) : null}58 <Row label="Method" mono>59 {req.method}60 </Row>61 <Row label="Domain">{req.domain}</Row>62 <Row label="Format" mono>63 {req.format}64 </Row>65 <Row label="Source">66 <Badge variant="outline">{req.source}</Badge>67 {req.browser ? (68 <Badge variant="warning" className="ml-1.5">69 browser requested70 </Badge>71 ) : null}72 </Row>73 <Row label="Project">74 {req.projectName}75 {otherProject ? <span className="ml-1.5 text-[11.5px] text-fg-subtle">(not the current project)</span> : null}76 </Row>77 <Row label="API key">78 {req.apiKey ? (79 <span className="inline-flex flex-wrap items-center gap-1.5">80 <Link href="/dashboard/api-keys" className="underline-offset-4 hover:underline">81 {req.apiKey.name}82 </Link>83 <code className="font-mono text-[12px] text-fg-muted">84 {req.apiKey.prefix}…{req.apiKey.last4}85 </code>86 {req.apiKey.revoked ? <Badge variant="danger">revoked</Badge> : null}87 </span>88 ) : (89 <span className="text-fg-subtle">{req.source === "playground" ? "Playground (no key)" : "—"}</span>90 )}91 </Row>92 <Row label="Session">93 {req.sessionId ? (94 <Link href="/dashboard/sessions" className="font-mono text-accent underline-offset-4 hover:underline">95 {req.sessionId}96 </Link>97 ) : (98 <span className="text-fg-subtle">none</span>99 )}100 </Row>101 </dl>102 </Card>103 <Card>104 <CardHeader className="pb-1">105 <CardTitle className="text-[14px]">Routing and result</CardTitle>106 </CardHeader>107 <dl className="divide-y divide-border">108 <Row label="Network">109 <NetworkLabel network={req.network} requested={req.requestedNetwork} />110 <span className="ml-1.5 text-[11.5px] text-fg-subtle">requested {req.requestedNetwork}</span>111 </Row>112 <Row label="Location" mono>113 {[req.country, req.region, req.city].filter(Boolean).join(" · ") || <span className="font-sans text-fg-subtle">any</span>}114 </Row>115 <Row label="HTTP status">116 <HttpCode code={req.httpStatus} />117 </Row>118 <Row label="Latency" mono>119 {formatMs(req.latencyMs)}120 </Row>121 <Row label="Attempts" mono>122 {req.attempts}123 </Row>124 <Row label="Bytes in / out" mono>125 {formatBytes(req.bytesIn)} / {formatBytes(req.bytesOut)}126 </Row>127 <Row label="Price" mono>128 {formatUsd(req.priceUsd, true)}129 {req.cached ? <Badge variant="info" className="ml-1.5 font-sans">cached</Badge> : null}130 </Row>131 {req.errorCode ? (132 <Row label="Error" mono>133 <span className="text-danger">{req.errorCode}</span>134 {req.errorMessage ? <div className="mt-0.5 font-sans text-fg-muted">{req.errorMessage}</div> : null}135 </Row>136 ) : null}137 <Row label="Completed">{req.completedAt ? formatDate(req.completedAt, { timeStyle: "medium" }) : <span className="text-fg-subtle">pending</span>}</Row>138 </dl>139 </Card>140 </div>141 );142143 return (144 <div className="flex flex-col gap-5">145 <Link href="/dashboard/requests" className="inline-flex items-center gap-1 text-[12.5px] text-fg-muted underline-offset-4 hover:text-fg hover:underline">146 <ArrowLeft className="size-3.5" /> Requests147 </Link>148 <PageHeader149 eyebrow="Request"150 title={151 <span className="inline-flex flex-wrap items-center gap-2">152 <span className="font-mono text-[18px] sm:text-[20px]">{req.id}</span>153 <CopyButton value={req.id} />154 <StatusBadge status={req.status} />155 </span>156 }157 description={158 <>159 {formatDate(req.createdAt, { timeStyle: "medium" })} ({timeAgo(req.createdAt)}) · {req.method} {req.domain}160 </>161 }162 actions={163 <Button asChild variant="outline" size="sm">164 <Link href={`/dashboard/requests?id=${encodeURIComponent(req.id)}`}>Find in log</Link>165 </Button>166 }167 />168169 {req.status === "failed" && errorHelp ? (170 <Alert variant="danger" title={req.errorCode ?? "Request failed"}>171 {req.errorMessage ?? errorHelp}172 {req.errorMessage && req.errorMessage !== errorHelp ? <span className="block text-fg-subtle">{errorHelp}</span> : null}173 </Alert>174 ) : null}175176 <div className="grid gap-5 lg:grid-cols-[1fr_280px]">177 <div className="min-w-0">178 <RequestDetailTabs179 attemptCount={req.attemptRows.length}180 headerCount={headerCount}181 overview={overview}182 attempts={183 <Card>184 <CardHeader>185 <CardTitle className="text-[14px]">Attempts</CardTitle>186 <CardDescription>Each route the router tried, in order. A blocked route escalates to another network or IP automatically.</CardDescription>187 </CardHeader>188 <CardContent>189 <AttemptsTimeline attempts={req.attemptRows} providerVisibility={ws.organization.providerVisibility} />190 </CardContent>191 </Card>192 }193 headers={<HeadersTables request={req.requestHeaders} response={req.responseHeaders} />}194 timing={<TimingWaterfall timing={req.timing} totalMs={req.latencyMs} />}195 />196 </div>197 <aside className="flex flex-col gap-4">198 <Card>199 <CardHeader className="pb-2">200 <CardTitle className="flex items-center gap-2 text-[14px]">201 <LifeBuoy className="size-4 text-fg-subtle" aria-hidden /> Need help?202 </CardTitle>203 </CardHeader>204 <CardContent className="text-[13px] text-fg-muted">205 Include this request ID when contacting{" "}206 <a href={`mailto:support@fetcha.co?subject=${encodeURIComponent(`Request ${req.id}`)}`} className="text-accent underline-offset-4 hover:underline">207 support@fetcha.co208 </a>209 . It lets us trace every attempt without you sharing the URL or headers.210 <div className="mt-3 flex items-center gap-1 rounded-md border border-border bg-bg-subtle px-2 py-1 font-mono text-[12px] text-fg">211 <span className="truncate">{req.id}</span>212 <CopyButton value={req.id} className="ml-auto" />213 </div>214 </CardContent>215 </Card>216 <Card>217 <CardHeader className="pb-2">218 <CardTitle className="text-[14px]">Reading this page</CardTitle>219 </CardHeader>220 <CardContent className="space-y-2 text-[12.5px] text-fg-muted">221 <p>222 <strong className="text-fg">Attempts</strong> greater than 1 means the first route was blocked or timed out and the router escalated. You are billed for the bytes that were actually transferred.223 </p>224 <p>225 <strong className="text-fg">Network</strong> is the class that served the final attempt. <code className="font-mono">auto</code> resolves to residential today.226 </p>227 </CardContent>228 </Card>229 </aside>230 </div>231 </div>232 );233}234